Cours 8 : Modèles de langues à n-grammes

Loïc Grobol lgrobol@parisnanterre.fr

2021-10-11

Pitch

On va apprendre un modèle de langues à n-grammes. On se basera pour la théorie et les notations sur le chapitre 3 de Speech and Language Processing de Daniel Jurafsky et James H. Martin (garde le donc pas loin). Notre objectif ici sera de faire du sampling.

Pour les données on va d'abord travailler avec Le Ventre de Paris qui est déjà dans ce repo pour les tests puis avec le corpus CIDRE pour passer à l'échelle, mais on pourrait aussi utiliser Wikipedia (par exemple en utilisant WikiExtractor) ou OSCAR.

On va devoir faire les choses suivantes (pour un modèle à bigrammes)

On va essayer de faire les choses à la main, sans trop utiliser de bibliothèques, pour bien comprendre ce qui se passe.

Premier prototype.

On va commencer par faire en entier le cas des bigrammes sur Le Ventre de Paris et on généralisera ensuite.

Lire et compter

On commence par lire un fichier et en extraire les unigrammes (ce qui nous donne le vocabulaire) et les bigrammes. On va pour l'instant faire ça très basiquement avec une bête tokenisation sur les espaces et les signes de ponctuation.

Vous voyez pour quoi on ne fait pas simplement un split() ?

(Si vous trouvez zip(words[:-1], words[1:]) obscur, faites quelques tests pour voir pourquoi ça marche.)

Calculer les probas

On va ensuite estimer les probas de générer un certain mot $w_1$ sachant que le mot précédent est $w_0$. On le fait en utilisant la formule du maximum de vraissemblance:

\begin{equation} P(w_1|w_0) = \frac{\text{nombre d'occurences du bigramme $w_0 w_1$}}{\text{nombre d'occurrences de l'unigramme $w_0$}} \end{equation}

Pour que ce soit plus agréable à sampler on va utiliser un dictionnaire de dictionnaires : probs[v][w] stockera $P(w|v)$.

Un autre truc un peu pénible, c'est qu'en tenant compte de la casse comme on le fait, on sépare en deux les comptes de chaque mot (suivant qu'il se trouve ou non en début de phrase). C'est pas complètement une erreur, mais c'est un peu désagréable, on va normaliser tout ça.

Générer

Pour l'instant on ne va pas se préoccuper de sauvegarder le modèle on va l'utiliser directement pour sampler. Le principe est simple : on sample le premier mot, puis on sample le deuxième mot en prenant le premier qu'on vient de générer et ainsi de suite.

Est-ce que vous voyez le problème ?

Comment on sample le premier mot ?

Et quand est-ce qu'on décide de s'arrêter ?

On rouvre le bouquin et on trouve

We’ll first need to augment each sentence with a special symbol <s> at the beginning of the sentence, to give us the bigram context of the first word. We’ll also need a special end-symbol. </s>

Oups

Allez, on corrige

Il y a encore un petit problème

🤔

On a compté les lignes vides 😤. Ça ne posait pas de problème jusque-là puisque ça n'ajoutait rien aux compteurs de n-grammes, mais maintenant ça nous fait des ["<s>", "</s>"].

C'est reparti

Générer pour de vrai

Bon c'est bon maintenant ?

À peu près. On va pouvoir sampler.

Pour ça on va piocher dans le module random de la bibliothèque standard, et en particulier la fonction random.choices qui permet de tirer au sort dans une population finie en précisant les probabilités de chacun de éléments. Le poids n'ont en principe pas besoin d'être normalisés (mais ils le seront ici, évidemment).

Voyons déjà comment choisir le premier mot

Ça marche, maintenant une phrase ! On sample mot par mot et on s'arrête quand on arrive à </s>

C'est rigolo, hein ?

Qu'est-ce que vous pensez des textes qu'on génère ?

Les trigrammes

Avant de généraliser, on va voir comment passer aux trigrammes

Les n-grammes

On passe aux n-grammes ? On va essayer de les faire de façon un peu plus compacte.

Vous voyez un problème ?

Un peu d'originalité

Le modèle ici marche, mais comme le corpus est un peu petit, il manque souvent d'originalité pour des grandes valeurs de $n$. Il y a plusieurs façons d'y remédier et les sections 3.4 et 3.5 de *Speech and Language Processing donnent plus de détails à ce sujet.